package com.ideaaedi.springcloud.jd.gateway.config;

import com.ideaaedi.springcloud.jd.commonds.entity.BaseCodeMsgEnum;
import com.ideaaedi.springcloud.jd.commonds.entity.Result;
import com.ideaaedi.springcloud.jd.commonds.util.CommonUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Gateway异常处理器
 *
 * @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
 * @since 2021.0.1.A
 */
@RefreshScope
@Configuration
@EnableConfigurationProperties({ServerProperties.class})
public class GatewayExceptionHandler {
   
    @Value("${spring.application.name}")
    private String applicationName;
   
    @Getter
    private final RouteLocator routeLocator;
    
    private final ServerProperties serverProperties;
    
    private final ApplicationContext applicationContext;
    
    private final List<ViewResolver> viewResolvers;
    
    private final ServerCodecConfigurer serverCodecConfigurer;
    
    public GatewayExceptionHandler(ServerProperties serverProperties,
                                   ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                   ServerCodecConfigurer serverCodecConfigurer,
                                   RouteLocator routeLocator,
                                   ApplicationContext applicationContext) {
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.routeLocator = routeLocator;
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }
    
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler gatewayErrorWebExceptionHandler(ErrorAttributes errorAttributes) {
        JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes, this.serverProperties.getError(),
                this.applicationContext, applicationName);
        exceptionHandler.setViewResolvers(this.viewResolvers);
        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
    
    /**
     * (实际的)异常处理类
     *
     * @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
     */
    @Slf4j
    public static class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
        
        private final ApplicationContext applicationContext;
        private final String gatewayApplicationName;
        
        public JsonExceptionHandler(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
                                    ApplicationContext applicationContext, String gatewayApplicationName) {
            super(errorAttributes, new WebProperties.Resources(), errorProperties, applicationContext);
            this.applicationContext = applicationContext;
            this.gatewayApplicationName = gatewayApplicationName;
        }
        
        /**
         * 处理异常，并组装响应
         */
        @Override
       protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
            String errorTraceId = CommonUtil.getTraceId(request == null ? null : request.uri().getPath(), null);
            Throwable th = super.getError(request);
            log.error("traceId={}, GatewayExceptionHandler$JsonExceptionHandler has been invoked", errorTraceId, th);
            Map<String, Object> response = new HashMap<>(4);
            Result<Object> failure;
            String tips = th == null ? "" : th.getMessage();
            if (request == null) {
                log.info("curr request is null.");
                failure = Result.failure(tips, BaseCodeMsgEnum.GATEWAY_ROUTE_ERROR, tips);
            } else {
                GatewayExceptionHandler gatewayExceptionHandler = applicationContext.getBean(GatewayExceptionHandler.class);
                String path = request.uri().getPath();
                List<String> uriItemList =
                        Arrays.stream(path.split("/")).filter(StringUtils::isNotBlank).collect(Collectors.toList());
                if (uriItemList.size() <= 1) {
                    log.info("curr path -> {}", path);
                    failure =  Result.failure(tips, BaseCodeMsgEnum.GATEWAY_ROUTE_ERROR, tips);
                } else {
                    String microServiceName = uriItemList.get(0);
                    // 获取所有微服务id
                    Set<String> routeIdSet = new LinkedHashSet<>(16);
                    RouteLocator routeLocator = gatewayExceptionHandler.getRouteLocator();
                    routeLocator.getRoutes().subscribe(route -> {
                        String routeId = route.getId();
                        routeId = routeId.replace("ReactiveCompositeDiscoveryClient_", "");
                        routeIdSet.add(routeId);
                    });
                    if (!CollectionUtils.isEmpty(routeIdSet) && routeIdSet.contains(microServiceName)) {
                        failure = Result.failure(tips, BaseCodeMsgEnum.SERVICE_UNAVAILABLE_ERROR);
                    } else {
                        routeIdSet.remove(gatewayApplicationName);
                        failure = Result.failure("All micro service " + routeIdSet, BaseCodeMsgEnum.NON_EXIST_MATCHED_SERVICE_ERROR, microServiceName);
                    }
                }
            }
            response.put("code", failure.getCode());
            response.put("msg", failure.getMsg());
            response.put("data", failure.getData());
            response.put("traceId", errorTraceId);
            return response;
        }
        
        @Override
        protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
            // Render the error information as a JSON payload.
            // noinspection NullableProblems
            return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
        }
        
        @Override
        protected int getHttpStatus(Map<String, Object> errorAttributes) {
            return HttpStatus.BAD_GATEWAY.value();
        }
    }
}
